package top.lishuoboy.shiro02_springboot_jsp.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.exceptions.ExceptionUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.ShiroException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.subject.Subject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import top.lishuoboy.dependency.base.db.MyDbUtil;
import top.lishuoboy.shiro02_springboot_jsp.domain.User;
import top.lishuoboy.shiro02_springboot_jsp.mapper.UserMapper;
import top.lishuoboy.shiro02_springboot_jsp.service.ShiroService;

import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.sql.DataSource;
import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author lishuoboy
 * @date 2022/2/16 12:40
 */
@Slf4j
@Controller
public class ShiroController {
    @Autowired
    private DataSource ds;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    ShiroService shiroService;

    @GetMapping("hello_thymeleaf")
    private String helloThymeleaf(Model model) {
        log.info("hello_thymeleaf");
        model.addAttribute("msg", "hello shiro");
        return "hello_thymeleaf";
    }

    @GetMapping("hello_mp")
    @ResponseBody
    private Object helloMP() {
        resetDbData();

        Long userCount = userMapper.selectCount(null);
        log.debug("用户数量 userCount=={}", userCount);
        return "用户数量:" + userCount;

    }

    /**
     * 重置数据库表数据
     */
    private String resetDbData() {
        MyDbUtil.resetData(ds, "db_sql/lishuoboy-shiro.sql");
        return "重置成功";
    }


    /** 首页，访问无需认证 */
    @GetMapping({"/", "index"})
    private String index(@ModelAttribute("msg") String msg) {
        return "index";
    }

    /** 登录页，访问无需认证 */
    @GetMapping("/login")
    private String login(@ModelAttribute("loginMsg") String loginMsg, Model model) {
        model.addAttribute("loginMsg", loginMsg);
        return "login";
    }

    /** 登录认证，访问无需认证 */
    @PostMapping("/doLogin")
    private String doLogin(String name, String password, String captcha, RedirectAttributes redirectAttributes, HttpSession session) {
        log.debug("name=={}, password=={}, captcha=={}", name, password, captcha);
        UsernamePasswordToken token = new UsernamePasswordToken(name, password);
        Subject subject = SecurityUtils.getSubject();
        try {
            String captchaServer = (String) session.getAttribute("captcha");
            if (!captcha.equalsIgnoreCase(captchaServer)) {
                throw new AuthenticationException("验证码错误");
            }
            log.debug("认证状态, subject.isAuthenticated()=={}", subject.isAuthenticated());
            subject.login(token);  //用户登录认证
            session.setAttribute("uName", subject.getPrincipal());   // 登录成功的用户名存入 session
            /* 修改 session 有效期，默认30分，即 1800_000毫秒
                 若 shiro 不使用 redis 存储session，则以此为准
                 若 shiro  启用 redis 存储session，优先以 redis 设置的有效期（redisSessionDAO.setExpire(1800);）为准，否则以此为准。
             */
            subject.getSession().setTimeout(1800_000);
            log.debug("认证状态, subject.isAuthenticated()=={}", subject.isAuthenticated());
            return "redirect:main";
        } catch (UnknownAccountException e) {
            log.warn("认证失败: 用户名不存在~", e);
            redirectAttributes.addAttribute("loginMsg", "认证失败: 用户名不存在~");
        } catch (IncorrectCredentialsException e) {
            log.warn("认证失败: 密码错误~", e);
            redirectAttributes.addAttribute("loginMsg", "认证失败: 密码错误~");
        } catch (AuthenticationException e) {
            // AuthenticationException是很多认证异常的父类，包括:
            // UnknownAccountException(用户名不存在)、
            // IncorrectCredentialsException(密码错误)、
            // DisabledAccountException（帐号被禁用）、
            // LockedAccountException（帐号被锁定）等等
            // ExcessiveAttemptsException（登录失败次数过多）
            // ExpiredCredentialsException（凭证过期）等
            log.warn("认证失败", e);
            redirectAttributes.addAttribute("loginMsg", e.getMessage());
        }
        return "redirect:login";
    }

    /** 主页，访问需认证成功 */
    @GetMapping("main")
    private String main() {
        return "main";
    }

    /** 登出 */
    @GetMapping("logout")
    private String logout() {
        Subject subject = SecurityUtils.getSubject();
        log.debug("认证状态, subject.isAuthenticated()=={}", subject.isAuthenticated());
        subject.logout();
        log.debug("认证状态, subject.isAuthenticated()=={}", subject.isAuthenticated());
        return "redirect:index";
    }

    /**
     * 分别测试 登录 和 登出 后 是否可以访问任何资源。登录成功后可以访问所有。
     */
    @GetMapping("testIsAuthenticated")
    @ResponseBody
    private String testIsAuthenticated() {
        String msg = StrUtil.format("认证状态=={}", SecurityUtils.getSubject().isAuthenticated());
        log.debug(msg);
        return msg;
    }

    /** 注册 */
    @GetMapping("register")
    private String register() {
        return "register";
    }

    /** 注册，访问无需认证 */
    @PostMapping("doRegister")
    private String doRegister(String name, String password) {
        String salt = RandomUtil.randomString(10);
        log.debug("随机盐 salt=={}", salt);
        String pwd = new Md5Hash(password, salt, 1024).toHex();
        log.debug("加盐密码 pwd=={}", pwd);

        User user = new User().setName(name).setPwd(pwd).setSalt(salt);
        int insert = userMapper.insert(user);

        if (insert > 0) {
            log.info("注册成功，name=={}", name);
        }
//        return "redirect:main";  // 注册成功进入主页
//        return "redirect:/index";     // 注册成功，进入首页
        return "redirect:/login";     // 注册成功，进入首页

    }

    /** 检查注册用户是否存在 */
    @GetMapping("doCheckUserNameIsExists")
    @ResponseBody
    private Long doCheckUserNameIsExists(String name) {
        QueryWrapper queryWrapper = new QueryWrapper<User>().eq("name", name);
        Long count = userMapper.selectCount(queryWrapper);
        return count;
    }

    /** 1、代码方式授权。 测试前先认证（登录） */
    @GetMapping("testPermissionByCode")
    @ResponseBody
    private Object testPermissionByCode() {
        resetDbData();

        Subject subject = SecurityUtils.getSubject();

        if (subject.hasRole("admin")) {
            System.out.println("代码方式");
        }

        // 一、角色
        // 拥有一个角色 hasRole()，调用一次doGetAuthenticationInfo()
        log.info("hasRole()=={}", subject.hasRole("admin")); // true
        log.info("hasRole()=={}", subject.hasRole("other")); // false

        // 同时拥有n个角色 hasAllRoles()，调用n次doGetAuthenticationInfo()
        log.info("hasAllRoles()=={}", subject.hasAllRoles(Arrays.asList("admin", "user")));  // true
        log.info("hasAllRoles()=={}", subject.hasAllRoles(Arrays.asList("admin", "other"))); // false

        // 分别拥有n个角色hasRoles()结果列表，返回每个角色的拥有结果，调用n次doGetAuthenticationInfo()
        boolean[] isHasRoleArr = subject.hasRoles(Arrays.asList("admin", "user", "other"));
        log.info("isHasRoleArr=={}", isHasRoleArr);  // isHasRoleArr==[true, true, false]

        // 二、权限。基于权限字符串的访问控制  资源标识符:操作:资源类型
        // 拥有一个权限 isPermitted()，调用一次doGetAuthenticationInfo()
        log.info("isPermitted()=={}", subject.isPermitted("user:*:01"));        // true
        log.info("isPermitted()=={}", subject.isPermitted("user:insert:01"));   // true
        log.info("isPermitted()=={}", subject.isPermitted("user:remove"));      // false  相当于 user:remove:*

        log.info("isPermitted()=={}", subject.isPermitted("product:add:*"));    // true
        log.info("isPermitted()=={}", subject.isPermitted("product:add"));      // true  相当于 product:add:*
        log.info("isPermitted()=={}", subject.isPermitted("product:add:02"));   // true
        log.info("isPermitted()=={}", subject.isPermitted("product:remove"));   // false 相当于 product:remove:*

        // 同时拥有n个权限 isPermittedAll()，调用n次doGetAuthenticationInfo()
        boolean isPermittedAll = subject.isPermittedAll("user:*:01", "user:insert:01");
        log.info("isPermittedAll=={}", isPermittedAll);     // isPermittedAll==true

        // 拥有n个角色hasRoles()结果列表，返回每个角色的拥有结果，调用n次doGetAuthenticationInfo()
        boolean[] isPermittedArr = subject.isPermitted("user:*:01", "user:insert:01", "user:remove");
        log.info("isPermittedArr=={}", isPermittedArr);     // isPermittedArr==[true, true, false]

        return "看后台日志";
    }

    /**
     * 2、注解方式授权。当前@Controller类不成功,调用子类成功
     */
    @GetMapping("testPermissionByAnnotation")
    @ResponseBody
    private Object testPermissionByAnnotation() {
        log.info("testPermissionByAnnotation");
        StringBuilder sb = new StringBuilder();
        try {
            shiroService.requiresUser();
        } catch (Exception e) {
            sb.append("1、requiresUser 无权限异常，<br><br><br>");
        }
        try {
            shiroService.requiresGuest();
        } catch (Exception e) {
            sb.append("2、requiresGuest 无权限异常，<br><br><br>");
        }
        try {
            shiroService.requiresAuthentication();
        } catch (Exception e) {
            sb.append("3、requiresAuthentication 无权限异常，<br><br><br>");
        }
        try {
            shiroService.requiresRoles1();
        } catch (Exception e) {
            sb.append("4、requiresRoles1 无权限异常，<br><br><br>");
        }
        try {
            shiroService.requiresRoles2();
        } catch (Exception e) {
            sb.append("5、requiresRoles2 无权限异常，<br><br><br>");
        }
        try {
            shiroService.RequiresPermissions1();
        } catch (Exception e) {
            sb.append("6、RequiresPermissions1 无权限异常，<br><br><br>");
        }
        try {
            shiroService.RequiresPermissions2();
        } catch (Exception e) {
            sb.append("7、RequiresPermissions2 无权限异常，<br><br><br>");
        }
        return sb.toString();
    }

    /** 获取图形验证码 */
    @GetMapping("getCaptcha")
    private void getCaptcha(HttpServletResponse response, HttpSession session) throws IOException {
        //定义图形验证码的长和宽
        LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(200, 100, 1, 10);
        //图形验证码写出，可以写出到文件，也可以写出到流
        lineCaptcha.write(response.getOutputStream());
        // 将图片验证码值存入 session
        String code = lineCaptcha.getCode();
        log.debug("图片验证码code=={}", code);
        session.setAttribute("captcha", code);
        // 设置验证码过期删除，使之失效
        this.removeAttrbute(session, "captcha");
    }

    /**
     * 设置过时删除session中的验证码，一般1~3分钟
     *
     * @param session
     * @param attrName
     */
    private void removeAttrbute(final HttpSession session, final String attrName) {
        // 定时删除session中存的验证码
        new ScheduledThreadPoolExecutor(2).schedule(() -> session.removeAttribute(attrName), 10, TimeUnit.MINUTES);
    }

    /** shiro异常处理跳转页面。默认直接将异常信息输出到页面 */
    @ExceptionHandler(ShiroException.class)
    private String shiroExceptionHandler(Exception e, Model model) {
        log.error("shiro 异常", e);
        model.addAttribute("msg", "shiro 异常" + ExceptionUtil.stacktraceToString(e));
        return "redirect:index";
    }
}
